#!/usr/bin/python
# 作者:龚华
# 2023年07月15日15时53分36秒
# -*- coding:utf-8 -*-
from numpy import *
import operator
import matplotlib
import matplotlib.pyplot as plt

def createDate():
    group = array([[1.0,1.1],[1.0,1.0],[0,0],[0,0.1]]) # 4*2,4个样本，每个样本两个特征
    labels = ['A','A','B','B']   # 4个样本的标签,目标变量
    return group,labels
def classify0(inX,dataset,labels,k):
    """

    :param inX: 用于分类的输入向量，
    :param dataset: 输入的训练样本集
    :param lables: 标签向量
    :param k: 选择最近邻居的数目
    :return:
    """
    dataSetSize=dataset.shape[0] # 4

    # 计算距离
    # 把inX扩展成与dataset同样的维度，然后相减
    two_d_array = tile(inX,(dataSetSize,1))  # tile()函数将inX扩展成4*2的矩阵

    #print('输入的向量inX扩展dataset同样的维度:\n',two_d_array)

    # 两个矩阵相减,计算所有点到inX的距离之和
    diffMat = tile(inX,(dataSetSize,1)) - dataset # tile()函数将inX扩展成4*2的矩阵

    sqDiffMat = diffMat**2  # 平方,计算欧式距离
    sqDistances = sqDiffMat.sum(axis=1) # 按行求和

    distances = sqDistances**0.5 # 开根号
    sortedDistIndicies = distances.argsort() # 返回排序后的索引值，从小到大排序，返回的是索引值
                                            # argsort()函数返回的是数组值从小到大的索引值

    # 选择距离最小的k个点，统计每个标签出现的次数
    classCount = {} # 字典，字典的优点是o(1)时间复杂度查找，列表是o(n)
    for i in range(k):
        voteIlabel = labels[sortedDistIndicies[i]] #距离排序后的索引值，对应的标签

        # 字典的get()方法，返回指定键的值，如果值不在字典中返回默认值0
        #统计标签出现的次数
        classCount[voteIlabel] = classCount.get(voteIlabel,0)+1

    #把字典分解为元组列表 [(),(),()] ,然后按照第二个元素的次序对元组进行排序

    sortedClassCount = sorted(classCount.items(),key=operator.itemgetter(1),reverse=True) # 字典的items()方法，以列表返回可遍历的(键,值)元组数组
    #reverse=True 降序排序
    #key=operator.itemgetter(1) 按照第二个元素排序,classCount.items()返回的是(键,值)元组数组
    #key=operator.itemgetter(1)，既是字典的值排序

    return sortedClassCount[0][0] # 返回出现次数最多的标签，sortedClassCount是二维列表
                                # sortedClassCount[0]是一个元组，sortedClassCount[0][0]是元组的第一个元素，即标签

#第一步，准备数据，从文本文件中解析数据

#用于datingTestSet.txt文件
def file2matrix(fileName):
    """
    从文件中读取数据,解析为矩阵,返回特征矩阵和分类标签,特征矩阵是一个numpy矩阵,标签是一个列表,列表的元素是字符串,
    约会网站数据集,共有1000行,每行数据包含4项,分别是:三列为特征值,一列为目标变量，
    女孩子的喜欢程度分为三个级别,不喜欢,魅力一般,极具魅力
    datingTestSet.txt
    第一列：每年获取的飞行常客里程数
    第二列：为玩视频游戏所耗时间百分比
    第三列：每周消费的冰淇淋公升数
    """


    fr=open(fileName)
    arrayOLines=fr.readlines()
    numberOfLines=len(arrayOLines)  #文件的函数
    returnMat = zeros((numberOfLines,3))  #创建返回的numpy矩阵,numberOfLines行，3列,全部为0
    classLabelVector = []  #创建返回的标签列表
    index = 0
    # 解析文件数据到列表
    for line in arrayOLines:
        line =line.strip()  #截取掉所有的回车字符
        listFromLine = line.split('\t')  #然后使用tab字符\t将上一步得到的整行数据分割成一个元素列表。
                                        #split()函数是字符串的函数，返回的是一个列表
        returnMat[index,:] = listFromLine[0:3] #取前三个元素，存储到特征矩阵中,每行的前三个元素
                                                #returnMat[index,:]是第index行的所有元素
        #classLabelVector.append(int(listFromLine[-1]))  #索引值-1表示列表中的最后一列元素,将最后一列元素存储到标签列表中
        #原有的这个代码有问题，因为listFromLine[-1]是字符串，不能转换为int类型
        #修改为下面的代码
        labels={'didntLike':1,'smallDoses':2,'largeDoses':3}
        classLabelVector.append(labels[listFromLine[-1]]) #将最后一列元素存储到标签列表中
        index +=1

    return returnMat,classLabelVector

#用于datingTestSet2.txt文件
def file2matrix_person(fileName):
    """
    从文件中读取数据,解析为矩阵,返回特征矩阵和分类标签,特征矩阵是一个numpy矩阵,标签是一个列表,列表的元素是字符串,
    约会网站数据集,共有1000行,每行数据包含4项,分别是:三列为特征值,一列为目标变量，
    女孩子的喜欢程度分为三个级别,不喜欢,魅力一般,极具魅力
    datingTestSet.txt
    第一列：每年获取的飞行常客里程数
    第二列：为玩视频游戏所耗时间百分比
    第三列：每周消费的冰淇淋公升数
    """


    fr=open(fileName)
    arrayOLines=fr.readlines()
    numberOfLines=len(arrayOLines)  #文件的函数
    returnMat = zeros((numberOfLines,3))  #创建返回的numpy矩阵,numberOfLines行，3列,全部为0
    classLabelVector = []  #创建返回的标签列表
    index = 0
    # 解析文件数据到列表
    for line in arrayOLines:
        line =line.strip()  #截取掉所有的回车字符
        listFromLine = line.split('\t')  #然后使用tab字符\t将上一步得到的整行数据分割成一个元素列表。
                                        #split()函数是字符串的函数，返回的是一个列表
        returnMat[index,:] = listFromLine[0:3] #取前三个元素，存储到特征矩阵中,每行的前三个元素
                                                #returnMat[index,:]是第index行的所有元素
        classLabelVector.append(int(listFromLine[-1]))  #索引值-1表示列表中的最后一列元素,将最后一列元素存储到标签列表中

        # 由于datingTestSet2.txt中的数据的最后一列是已经转换为数字的，所以这里不需要转换
        #labels={'didntLike':1,'smallDoses':2,'largeDoses':3}
        #classLabelVector.append(labels[listFromLine[-1]]) #将最后一列元素存储到标签列表中
        index +=1

    return returnMat,classLabelVector

#画图 4个散点图
def showdatas(datingDataMat,datingLabels):
    """
    画图
    用datingDataMat矩阵的第二、第三列数据画散点图.
    画出的图是两个特征值的散点图，横轴是玩视频游戏所耗时间百分比，纵轴是每周消费的冰淇淋公升数
    无法得出很明显的结论，因为两个都是一个颜色，无法区分
    """
    fig = plt.figure()
    ax = fig.add_subplot(111)
    ax.scatter(datingDataMat[:,1],datingDataMat[:,2]) # 画散点图
    ax.set_xlabel('玩视频游戏所耗时间百分比',fontproperties='SimHei',fontsize=15)
    ax.set_ylabel('每周消费的冰淇淋公升数',fontproperties='SimHei',fontsize=15)
    plt.savefig('./picture/my_plot.png', dpi=300)

def showdatas1(datingDataMat,datingLabels):
    """
    画图，上颜色
    用datingDataMat矩阵的第二、第三列数据画散点图.
    画出的图是两个特征值的散点图，横轴是玩视频游戏所耗时间百分比，纵轴是每周消费的冰淇淋公升数
    我们ax.scatter(datingDataMat[:,1],datingDataMat[:,2],15.0*array(datingLabels), 15.0*array(datingLabels))中的参数c就是color的缩写，
    也就是点的颜色，这里我们用变量datingLabels存储的标签属性作为色彩属性值，这样就可以用色彩区分不同标签的人群了。

    结论：虽然可以用颜色区分，但是很难根据这个图得出什么结论。
    """
    fig = plt.figure()
    ax = fig.add_subplot(111)
    ax.scatter(datingDataMat[:,1],datingDataMat[:,2],15.0*array(datingLabels), 15.0*array(datingLabels)) # 画散点图
    ax.set_xlabel('玩视频游戏所耗时间百分比',fontproperties='SimHei',fontsize=15)
    ax.set_ylabel('每周消费的冰淇淋公升数',fontproperties='SimHei',fontsize=15)
    plt.savefig('./picture/my_plot1.png', dpi=300)
def showdatas2(datingDataMat, datingLabels):
    """
    画图，上颜色
    用datingDataMat矩阵的第一、第二列数据画散点图.

    结论：虽然可以用颜色区分，但是很难根据这个图得出什么结论。
    """
    fig = plt.figure()
    ax = fig.add_subplot(111)
    ax.scatter(datingDataMat[:,0],datingDataMat[:,1],15.0*array(datingLabels), 15.0*array(datingLabels)) # 画散点图
    #黄色是3，绿色是2,紫色是1,这个是根据datingLabels的值来的
    #分别对应的是largeDoses，smallDoses，didntLike
    #15.0*array(datingLabels)是点的大小，15.0*array(datingLabels)是点的颜色
    #这里的datingLabels是一个列表，里面的元素是1，2，3，所以15.0*array(datingLabels)是一个列表，里面的元素是15，30，45
    ax.set_xlabel('玩视频游戏所耗时间百分比',fontproperties='SimHei',fontsize=15)
    ax.set_ylabel('每年获取的飞行常客里程数',fontproperties='SimHei',fontsize=15)
    plt.savefig('./picture/my_plot2.png')


    plt.savefig('my_plot3.png', dpi=300)
def showdatas3(datingDataMat, datingLabels):


    matrix = datingDataMat
    labels = datingLabels

    zhfont = matplotlib.font_manager.FontProperties(fname='C:\Windows\Fonts\simkai.ttf')

    """ 比较好看的绘制方法 """

    plt.figure(figsize=(8, 5), dpi=80)
    axes = plt.subplot(111)
    # 将三类数据分别取出来
    # x轴代表飞行的里程数
    # y轴代表玩视频游戏的百分比
    type1_x = []
    type1_y = []
    type2_x = []
    type2_y = []
    type3_x = []
    type3_y = []

    for i in range(len(labels)):
        if labels[i] == 1:  # 不喜欢
            type1_x.append(matrix[i][0])
            type1_y.append(matrix[i][1])

        if labels[i] == 2:  # 魅力一般
            type2_x.append(matrix[i][0])
            type2_y.append(matrix[i][1])

        if labels[i] == 3:  # 极具魅力
            type3_x.append(matrix[i][0])
            type3_y.append(matrix[i][1])

    type1 = axes.scatter(type1_x, type1_y, s=20, c='red')
    type2 = axes.scatter(type2_x, type2_y, s=40, c='green')
    type3 = axes.scatter(type3_x, type3_y, s=50, c='blue')
    # plt.scatter(matrix[:, 0], matrix[:, 1], s=20 * numpy.array(labels),
    #             c=50 * numpy.array(labels), marker='o',
    #             label='test')
    plt.xlabel(u'每年获取的飞行里程数', fontproperties=zhfont)
    plt.ylabel(u'玩视频游戏所消耗的事件百分比', fontproperties=zhfont)
    axes.legend((type1, type2, type3), (u'不喜欢', u'魅力一般', u'极具魅力'), loc=2, prop=zhfont)

    plt.savefig('./picture/my_plot3.png', dpi=300)


#归一化特征值
def autoNorm(dataSet):
    """
    归一化特征值，消除特征之间量级不同导致的影响
    三个特征值的范围不一样，有的是0-1，有的是1-1000，有的是1-100000
    每一列的大小值，都算出出来，然后用公式进行归一化
    归一化的时候，用矩阵减法和除法，而不是循环，因为numpy对矩阵的操作比较快
    直接用矩阵减法和特征值除法，就可以把矩阵的每一列都归一化了
    numpy中的矩阵除法是调用linalg.solve(matA,matB)来求解
    返回值：归一化后的矩阵，范围值，最小值

    """

    minVals = dataSet.min(0) #取每一列的最小值,三个特征值的最小值，是一个列表
    maxVals = dataSet.max(0) #取每一列的最大值，三个特征值的最大值，是一个列表
    ranges = maxVals - minVals #最大值减去最小值
    normDataSet = zeros(shape(dataSet)) #创建一个和dataSet一样大小的矩阵，但是里面的值都是0
    m = dataSet.shape[0] #取dataSet的行数
    #公式为：newValue = (oldValue-min)/(max-min)
    #第一步：减去最小值，矩阵减法
    normDataSet = dataSet - tile(minVals,(m,1)) #tile(minVals,(m,1))是把minVals复制成m行1列的矩阵
    #第二步：除以最大值和最小值的差值，矩阵除法
    normDataSet = normDataSet/tile(ranges,(m,1)) #归一化，tile(ranges,(m,1))是把ranges复制成m行1列的矩阵
    return normDataSet,ranges,minVals  #返回归一化后的矩阵，范围值，最小值，这三个值都是列表
                                    #ranges,minVals是列表，但是只有一行，因为只有一列
                                    #在归一化测试数据的时候，需要用到这三个值，所以返回这三个值

#4.分类器针对约会网站的测试代码
def datingClassTest():
    """
    分类器针对约会网站的测试代码
    """
    hoRatio = 0.10  #测试数据的比例
    datingDataMat,datingLabels = file2matrix('datingData/datingTestSet.txt') #读取文件数据
    normMat,ranges,minVals = autoNorm(datingDataMat) #归一化特征值



    m = normMat.shape[0] #取normMat的行数
    numTestVecs = int(m*hoRatio) #取测试数据的数量
    errorCount = 0.0 #错误率

    #一个个样本进行测试，一个样本就是一行数据，一行数据有三个特征值，一个标签
    #把数据分成两部分，一部分是测试数据，一部分是训练数据
    #训练数据集是数据的90%，测试数据集是数据的10%
    #测试数据集的每一行数据，都拿去和训练数据集的数据进行比较，看看是哪个标签的数据最相似
    #如果最相似的标签和测试数据集的标签不一样，说明预测错误，错误率加1
    #最后算出错误率，错误率越低，说明分类器越好



    for i in range(numTestVecs): #循环测试数据
        #前numTestVecs个数据作为测试数据，后面的数据作为训练数据
        #normMat[numTestVecs:m,:]是取numTestVecs到m行的所有列
        #datingLabels[numTestVecs:m]是取numTestVecs到m行的第一列
        #3是K值，即前3个最相似的数据

        #classify0函数是分类器，输入测试数据，训练数据，训练数据的标签，K值
        classifierResult = classify0(normMat[i,:],normMat[numTestVecs:m,:],\
                                     datingLabels[numTestVecs:m],3)
        print("the classifier came back with: %d, the real answer is: %d"\
              %(classifierResult,datingLabels[i]))
        if (classifierResult != datingLabels[i]): #如果预测结果和实际结果不一样
            errorCount += 1.0 #错误率加1
    print("the total error rate is: %f" % (errorCount/float(numTestVecs))) #打印错误率
    print(errorCount)

# 5.约会网站预测函数
def classifyPerson():
    resultList=['not at all','in small doses','in large doses'] #标签列表
    percentTats = float(input("percentage of time spent playing video games?(input 10)：")) #输入第一个特征值
    ffMiles = float(input("frequent flier miles earned per year?(input 10000)：")) #输入第二个特征值
    iceCream = float(input("liters of ice cream consumed per year?(input 0.5)：")) #输入第三个特征值
    datingDataMat,datingLabels = file2matrix_person('datingData/datingTestSet2.txt') #读取文件数据
    #网站的数据归一化
    normMat,ranges,minVals = autoNorm(datingDataMat) #归一化特征值

    #输入的特征值列表
    inArr = array([ffMiles,percentTats,iceCream]) #输入的特征值列表

    #三次输入的特征值要归一化，才能和训练数据进行比较
    inArr=(inArr-minVals)/ranges #输入的特征值列表减去最小值，除以范围值，得到归一化后的特征值列表
    classifierResult = classify0(inArr,normMat,datingLabels,3)
            #调用分类器，输入归一化后的特征值列表，训练数据，训练数据的标签，K值
    print(classifierResult)
    print("You will probably like this person: ",\
          resultList[classifierResult - 1]) #打印结果



def data_prepare():
    group,labels = createDate()
    print(group)
    print(labels)

    predict=classify0([0,0],group,labels,3)
    print('predict=:',predict)

    # 读取文件数据
    datingDataMat,datingLabels = file2matrix('datingData/datingTestSet.txt')
    print('datingDataMat=\n',datingDataMat)
    print('datingLabels=\n',datingLabels)

    # 画图
    showdatas(datingDataMat,datingLabels)  #画图，颜色都是一个颜色
    showdatas1(datingDataMat,datingLabels) #画图，颜色不同，但是很难根据这个图得出什么结论，
                                          #也就是第二列和第三列的数据，无法得出很明显的结论
    showdatas2(datingDataMat,datingLabels) #画图，颜色不同，第一列和第二列的特征值，可以得出很明显的结论
    ##黄色是3，绿色是2,紫色是1,这个是根据datingLabels的值来的
    showdatas3(datingDataMat,datingLabels)
def show_autoNorm():
    datingDataMat,datingLabels = file2matrix('datingData/datingTestSet.txt')
    normMat,ranges,minVals = autoNorm(datingDataMat)
    print(normMat[0:10,:])  #打印归一化后的矩阵,元素值都在0-1之间，float类型
    print(type(normMat[0,0])) #打印元素的类型，是float类型
    print(ranges) #打印范围值，是列表
    print(minVals) #打印最小值，是列表

if __name__=='__main__':

    #data_prepare()
    #打印归一化后的矩阵，范围值，最小值
    #show_autoNorm()

    #测试分类器
    #datingClassTest()

    #约会网站预测函数
    classifyPerson()

